Sfey REST API v3 Design Principles
This document defines the REST design principles governing Sfey REST API v3 surfaces that are
documented under reference/v3.0/. The TPS AFC API (reference/v3.0/tps-afc-api.yaml) is the
normative example: production base URL https://tps.sfey.com/afc-api/v3 (see servers in that
spec). All endpoints and OpenAPI specifications in the versioned reference must conform to these
rules.
1. Resource Model and URL Structure
1.1 Base Path
Base path structure is: /{api}/v{versionMajor}/… (for example afc-api as the {api} segment
for the TPS AFC API).
Currently, the following v3.0 OpenAPI root documents exist under reference/v3.0/:
tps-afc-api.yaml— TPS AFC API (transaction processing and card management for AFC integration; resource paths below live under this document’sserversURL).tps-afc-callback-api.yaml— TPS AFC Callback API (webhook contract implemented by the AFC backend); path layout differs from the REST API and is not covered in detail here.
This document covers requirements for API major version 3 as implemented by tps-afc-api.yaml.
Every subresource is accessed under the company namespace: /company/{companyId}/….
The TPS AFC API exposes the following resource types:
| Resource | Parent resource | Unique ID | Path namespace | Comment |
|---|---|---|---|---|
| Company | — | Company ID | /company/{companyId} | Namespace only; company is not a standalone resource in this spec—only its subresources are addressable |
| Transaction | Company | transactionUuid (UUID) | /company/{companyId}/transaction, /company/{companyId}/transaction/{transactionUuid} | Payments, refunds, and debt recoveries; operation chosen via transactionType in the body (POST) or in the returned resource |
| Card | Company | ICC certificate hash | /company/{companyId}/card/by-icchash/{iccHash} | Primary AFC card lookup: iccHash = Base64Url(SHA256(EMV Tag 9F46)) |
| Card | Company | (search) | /company/{companyId}/card | Card search by PAN fragments and optional filters (GET) |
| Event | Company | iccHash (ICC hash) | /company/{companyId}/event | Deny-list events for a card; iccHash is a required query parameter (ICC hash), see tps-afc-api.yaml CardId |
1.2 Path Naming
| Rule | Convention | Example |
|---|---|---|
| Segment casing | lowercase kebab-case | /by-icchash |
| Resource names | singular nouns | …/transaction, …/card, …/event |
| Sub-resources | under company | /company/{companyId}/transaction/{transactionUuid} |
| No trailing slashes | paths never end with / | …/card/{iccHash} ✓ …/card/{iccHash}/ ✗ |
| No verbs in paths | actions expressed via HTTP methods | POST …/transaction not …/create-transaction |
| No file extensions | content negotiation via Accept | …/transaction not …/transaction.json |
1.3 Path Parameters
- Use
camelCasefor parameter names:companyId,transactionUuid,iccHash. - Every path parameter must be
required: true. - Type and format must be explicit (e.g.
integer/int64,string/uuid). - Parameters that are identifiers should use the most specific type available:
companyId→integer/int64,transactionUuid→string/uuid,iccHash→string.
2. HTTP Methods
| Method | Semantics | Request body | Safe | Idempotent |
|---|---|---|---|---|
| GET | Retrieve a resource | No | Yes | Yes |
| POST | Create a resource / action | Yes | No | No* |
| PUT | Full replace of a resource | Yes | No | Yes |
| DELETE | Remove a resource | No | No | Yes |
* POST createTransaction relies on the client transaction id in transactionData.uuid for
idempotent behaviour via 409 Conflict on duplicates (see §6).
Method Selection Rules
- Retrieval — always GET, never POST-to-query. Exception: search endpoints that would exceed URL length limits may use POST with a request body, but GET with query parameters is preferred when feasible. Also, search endpoints containing secret information in their parameters should use POST.
- Creation —
POSTto the collection resource (POST /company/{companyId}/transactionintps-afc-api.yaml). The request body usesCreateTransactionRequest; the client-supplied transaction id istransactionData.uuidincomponents/schemas/TransactionData.yaml(the operation text intps-afc-api.yamlrefers to the same value as the idempotency key). - Full replacement — PUT is not defined on
tps-afc-api.yamlpaths; reserved if added later. - Deletion — DELETE is not defined on
tps-afc-api.yamlpaths; reserved if added later.
3. Request Conventions
3.1 Content Type
- Request bodies:
application/json - No
multipart/form-dataorapplication/x-www-form-urlencoded
3.2 Request Body Rules
- GET and DELETE requests must not have a body.
- POST and PUT requests must have a body (even if minimal).
- All fields use
camelCasenaming. - Required fields must be marked as
requiredin the schema. - Optional fields should have sensible defaults documented in the
description.
3.3 Query Parameters
- Used for filtering, sorting, and pagination on GET collection endpoints.
- Use
camelCasenaming. - Array-valued parameters use repeated keys where applicable, e.g.
?actionCodes=A&actionCodes=Bfor theactionCodesarray onlistTransactionsintps-afc-api.yaml. - Date/time parameters use ISO 8601 format and should support time zone information:
2025-01-20T22:00:00Z.
4. Response Conventions
4.1 Content Type
- All responses:
application/json
4.2 Success Status Codes
| Code | When to use |
|---|---|
| 200 | GET success (list, get-by-id, and success responses with a body) |
| 201 | POST createTransaction — resource created |
| 204 | Not used in tps-afc-api.yaml (no success response without a body) |
4.3 Response Body Rules
- 200 and 201 responses return a JSON body as defined for each operation in
tps-afc-api.yaml(seecomponents/responses/*). - Field naming:
camelCase, matching request conventions. - Null fields should be omitted rather than sent as
null, unless the distinction between absent and null carries semantic meaning. - Timestamps use ISO 8601 with UTC offset:
2025-06-15T14:30:00Z. - Monetary amounts use
integer/int32(minor unit, e.g. öre) with an accompanyingcurrencyCodeNumfield (integer/int32, ISO 4217 numeric code), as stated intps-afc-api.yamlinfo.description.
5. Error Handling
5.1 Error Status Codes
| Code | Meaning | When to use |
|---|---|---|
| 400 | Bad Request | Malformed JSON, missing required fields, constraint violations |
| 401 | Unauthorized | Missing or invalid authentication credentials |
| 403 | Forbidden | Authenticated but insufficient permissions |
| 404 | Not Found | Resource identified by path parameters does not exist |
| 409 | Conflict | Duplicate client transaction id (transactionData.uuid) for create, or other conflict (see §6) |
| 422 | Unprocessable Entity | Syntactically valid request that fails business validation |
| 500 | Internal Server Error | Unexpected server failure (should be minimised) |
| 502 | Bad Gateway | Upstream service returned an invalid response |
| 503 | Service Unavailable | Server temporarily unable to handle requests |
| 504 | Gateway Timeout | Upstream service did not respond in time |
5.2 Error Response Schema
In tps-afc-api.yaml, most error responses reference components/responses/ErrorResponse, which
uses the shared schema components/schemas/ErrorResponse.yaml:
# reference/v3.0/components/schemas/ErrorResponse.yaml
type: object
description: Error message
properties:
systemCode:
type: integer
format: int32
description: Error code
systemCodeDescription:
type: string
description: Error description
401 uses AuthenticationErrorResponse (adds a WWW-Authenticate header) with the same JSON
body schema. Client and server errors are grouped in the spec with '4XX' and '5XX' response
entries that both reference this ErrorResponse pattern.
5.3 Error Code Ranges
| Range | Owner | Example |
|---|---|---|
| 1000–1999 | Validation errors | 1001: Missing required field |
| 2000–2999 | Authentication/authz | 2001: Token expired |
| 3000–3999 | Resource errors | 3001: Transaction not found |
| 4000–4999 | Business-rule errors | 4001: Unprocessable tap (AFC integration compat) |
| 5000–5999 | Upstream/infra | 5001: Card network timeout |
6. Idempotency
POST /company/{companyId}/transaction uses the create request schema
components/schemas/CreateTransactionRequest.yaml. The client-supplied transaction identifier is
transactionData.uuid (string / uuid in TransactionData.yaml). The createTransaction
description in tps-afc-api.yaml states that the same UUID must not be reused for a different
transaction: an existing transaction with that identifier results in 409 Conflict.
There is no PUT or DELETE on the paths in tps-afc-api.yaml. GET methods are safe and
idempotent by HTTP semantics.
7. Pagination
tps-afc-api.yaml uses two collection patterns:
7.1 Transaction list (GET /company/{companyId}/transaction)
Query parameters (see listTransactions in tps-afc-api.yaml):
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer (int32) | yes | Page number (zero-based) |
size | integer (int32) | yes | Page size |
Other filters (startDateTime, endDateTime, panToken, transactionUuid, transactionType,
actionCodes, clearingSequenceNumber, direction, sort) apply as documented on the
operation. Note: sort is marked deprecated in the spec in favour of the comma-separated
fields described on that parameter.
7.2 Card search (GET /company/{companyId}/card)
Offset-based pagination:
| Parameter | Type | Default | Constraints | Description |
|---|---|---|---|---|
offset | integer (int32) | 0 | minimum 0 | Number of items to skip |
limit | integer (int32) | 20 | 1–100 | Max items to return |
7.3 Paginated response envelope
List responses (ListTransactionsResponse, ListCardsResponse, ListEventsResponse) share the
same shape: an array data plus a page object defined by components/schemas/PageData.yaml:
# PageData — summary
properties:
pageNumber: # current page (0-based)
pageSize:
hasNext:
filters: # echo of request filters (object, additionalProperties)
This replaces older content / offset / limit / hasMore naming used in earlier drafts.
8. Filtering and Sorting
8.1 Card search (GET /company/{companyId}/card)
- PAN fragments:
panFirstDigits,panLastDigits(optional; any combination allowed per spec). - Card expiry filter:
expiryYearMonth(ISO year-month,YYYY-MM). - Created range:
createdStart,createdEnd(ISO 8601 date-time, UTC). - Sorting:
sort(field name, e.g.created) anddirection(ASC/DESC, defaultDESCpertps-afc-api.yamlparameter definitions).
8.2 Transaction list (GET /company/{companyId}/transaction)
- Required window:
startDateTime,endDateTime(ISO 8601 strings as described on the operation). - Optional filters:
panToken,transactionUuid,transactionType,actionCodes,clearingSequenceNumber. - Sorting: optional
direction(ASC/DESC);sortis deprecated — see the operation for the comma-separated sort fields array.
8.3 General rules
- Date range filters for card search use a
Start/Endsuffix on the field name (createdStart,createdEnd).
9. Versioning
- The version is embedded in the URL path:
/afc-api/v3/…(seeserversintps-afc-api.yaml). - Major version increments indicate breaking changes.
- Non-breaking additions (new optional fields, new endpoints) do not require a version bump.
- The
info.versionfield follows semantic versioning and may include pre-release labels for work in progress (for example3.0.0-alpha1intps-afc-api.yaml).
10. Security
- All endpoints require OAuth 2.0 Bearer tokens.
- The token is passed in the
Authorization: Bearer <token>header. - The OpenAPI document (
tps-afc-api.yaml) declarescomponents.securitySchemes.bearerAuthas typeoauth2with theclientCredentialsflow (tokenUrl: https://tps.sfey.com/oauth/token,scopes: {}) and applies it globally viasecurity: [ { bearerAuth: [] } ]. - No API keys, basic auth, or cookie-based auth.
11. Naming Conventions Summary
| Element | Convention | Example |
|---|---|---|
| Path segments | kebab-case | /by-icchash |
| Path parameters | camelCase | {companyId} |
| Query parameters | camelCase | ?panFirstDigits=4111 |
| Request/response fields | camelCase | transactionUuid |
| Schema names | PascalCase | CreateTransactionRequest |
| Enum values | UPPER_SNAKE | TRANSIT_VALIDATION_PRICE |
| Header names | Standard HTTP | Authorization, Content-Type |
12. OpenAPI Specification File Structure
12.1 Directory Layout
The reference/ directory is the root for all OpenAPI specifications. Its structure mirrors the
OpenAPI document model so that file paths are predictable from the $ref they resolve.
reference/
├── old/ # legacy / pre-versioned specs (flat layout)
| └── <legacy-spec>.yaml
└── v<major>.<minor>/ # e.g. v3.0 — versioned API surface
├── tps-afc-api.yaml # TPS AFC API (TPS, AFC integration)
├── tps-afc-callback-api.yaml # AFC backend webhook contract (separate base URL)
└── components/
└── schemas/
├── CreateTransactionRequest.yaml
├── ErrorResponse.yaml
├── PageData.yaml
├── TransactionData.yaml
├── CardData.yaml
└── … # other domain and response payloads
12.2 Version Directories
| Rule | Convention | Example |
|---|---|---|
| Directory name | v<major>.<minor> | v3.0 |
| One directory per API version | A new minor/major version gets its own directory | v3.0/, v3.1/ |
| Legacy specs | Specs predating the versioned layout remain flat in reference/old/ | tps-api-v2.yaml |
The version segment in the directory name matches the info.version major.minor of the
contained specifications (e.g. v3.0/ contains specs with info.version: 3.0.x).
12.3 Root API Specification Files
Each root document lives directly inside its version directory. Naming rules:
| Rule | Convention | Example |
|---|---|---|
| File naming | <solution-name>-<api-name>.yaml | tps-afc-api.yaml, tps-afc-callback-api.yaml |
| Casing | lowercase kebab-case | tps-afc-api.yaml ✓, TpsAfcApi.yaml ✗ |
| Extension | .yaml (not .yml) | tps-afc-api.yaml |
| No version in filename | The version directory provides this context | tps-afc-api.yaml ✓, tps-afc-api-v3.yaml ✗ |
A root document contains the full OpenAPI skeleton: openapi, info, servers, tags,
security, paths (or webhooks), and components. Shared payload and domain schemas live in
components/schemas/*.yaml and are referenced with direct relative $refs from
components/requestBodies, components/responses, and from other schema files (see §12.5 — no
pass-through entries under components.schemas for file-backed shared types).
No inline request bodies or responses. In tps-afc-api.yaml, operations reference
components/requestBodies/* and components/responses/* via $ref rather than inlining full
requestBody / responses objects under each path. Success and error response payloads typically
$ref into components/schemas/*.yaml files (for example CreateTransactionRequest →
./components/schemas/CreateTransactionRequest.yaml).
12.4 Component Schema Files
Shared schemas (any type referenced from more than one place—another schema file, another
components/* entry, or more than one operation—or intended for reuse) must live in a
dedicated file under components/schemas/ inside the version directory. The file root is the
schema object (no SchemaName: wrapper key at the top of the file).
| Rule | Convention | Example |
|---|---|---|
| One schema per file | Each file defines a single top-level type: object (or enum) | CardData.yaml |
| File naming | PascalCase, matching the schema name | TransactionData.yaml, DeviceData.yaml |
| No wrapping key | The file root is the schema object — no CardData: wrapper | type: object at line 1 |
| Local sub-schemas | Use $defs (JSON Schema) for types scoped to a single file | TripData.yaml → $defs.TripStopPoint |
Internal $ref | Reference local sub-schemas as #/$defs/<Name> | $ref: '#/$defs/Coordinate' |
When to extract: In addition to the shared-schema rule above, a schema may be extracted to its
own file when it is complex enough that inlining it harms readability of the root document—even if
it is only referenced once—provided references to it remain direct file $refs as described in
§12.5 and §12.6.
12.5 Operation payloads vs shared domain schemas
Direct references only. Any consumer of a shared schema must $ref the YAML file with a
relative path (for example './components/schemas/CardData.yaml' from the root OpenAPI document,
or './CardData.yaml' from another file in components/schemas/). Do not add entries under
the root document’s components.schemas whose sole purpose is to point at an external file (a
pass-through or “re-export” such as SchemaName: { $ref: './components/schemas/SchemaName.yaml' }).
That pattern duplicates the name, obscures the real location in tooling, and is not allowed for
shared types. Prefer wiring requestBodies, responses, and other schema files straight to the
file under components/schemas/.
The root document may still use components.schemas for inline fragments that are not extracted
(rare) or for constructs that must remain in-document; it must not use that map as an indirection
layer for file-backed shared schemas.
tps-afc-api.yaml and sibling root specs combine inline root components with external schema
files as follows:
-
components/parameters,components/requestBodies, andcomponents/responsesare declared in the root OpenAPI document and referenced from each operation via$ref(for example#/components/requestBodies/CreateTransactionRequest). -
Payload structures for requests and responses are defined in
components/schemas/*.yamland referenced directly from those components (for example the request body schema$ref: './components/schemas/CreateTransactionRequest.yaml'). Shared domain types (TransactionData.yaml,CardData.yaml,DeviceData.yaml, …) compose each other with direct relative$refs between files undercomponents/schemas/. -
Reuse — The same schema file may be referenced from more than one
components/responses/*entry or from several payload files; each reference is a direct path to the file, not via an intermediate name undercomponents.schemas.
| Layer | Where it lives | Example |
|---|---|---|
| HTTP components | Root spec components/* | requestBodies/CreateTransactionRequest |
| Top-level payload | components/schemas/<Name>.yaml | CreateTransactionRequest.yaml |
| Domain objects | components/schemas/*.yaml | TransactionData.yaml |
12.6 $ref Usage
| Scope | Syntax | Example |
|---|---|---|
| Shared schema from root spec | Relative path to components/schemas/ | $ref: './components/schemas/ErrorResponse.yaml' (preferred; do not add a components.schemas alias that only repeats this $ref) |
Shared schema from another file in components/schemas/ | Relative path to sibling or nested file | $ref: './CardData.yaml', $ref: './DeviceKey.yaml' |
| Same document — request body | #/components/requestBodies/<Name> | $ref: '#/components/requestBodies/CreateTransactionRequest' |
| Same document — response | #/components/responses/<Name> | $ref: '#/components/responses/AuthenticationErrorResponse' |
| Sub-schema within external file | Relative path + JSON Pointer | $ref: './components/schemas/TripData.yaml#/$defs/Coordinate' |
| Within an extracted file | #/$defs/<Name> | $ref: '#/$defs/TripStopPoint' |
Avoid #/components/schemas/<Name> when <Name> is only a wrapper around a single $ref to a
file under components/schemas/; use the file path from the referencing context instead.
Circular $ref chains are not allowed. Every reference path must be resolvable with standard
JSON Reference / OpenAPI 3.1 resolution.
12.7 Request Bodies
Every operation that accepts a request body must declare it in components/requestBodies and
reference it via $ref. Inline request bodies under paths or webhooks are not permitted.
| Rule | Convention | Example |
|---|---|---|
| Naming | PascalCase; key under components/requestBodies | CreateTransactionRequest |
| Content type | application/json | |
required | Set explicitly on the request body component | required: true |
Schema $ref | Relative path to YAML under components/schemas/ | $ref: './components/schemas/CreateTransactionRequest.yaml' |
12.8 Responses
Every response declared on an operation must be defined in components/responses and referenced
via $ref. Inline response definitions under paths or webhooks are not permitted.
| Rule | Convention | Example |
|---|---|---|
| Success naming | PascalCase, outcome-oriented | CreateTransactionResponse, ListCardsResponse |
| Error naming | PascalCase; dedicated 401 response where needed | AuthenticationErrorResponse, ErrorResponse |
| Content type | application/json for JSON bodies | |
| Error schema | JSON body $ref: './components/schemas/ErrorResponse.yaml' (401 adds WWW-Authenticate header) | |
| Aggregates | tps-afc-api.yaml groups client/server errors as '4XX' / '5XX' entries | |
| Reuse | Define common responses once under components/responses and reuse across operations |
12.9 Future Component Types
The components/ directory may be extended with additional OpenAPI component types as the spec
evolves. New subdirectories follow the same pattern:
components/
├── schemas/ # reusable schemas
├── parameters/ # reusable path/query/header parameters (already in use in root specs)
├── responses/ # reusable response definitions (may be extracted to files in the future)
├── requestBodies/ # reusable request body definitions (may be extracted to files in the future)
└── examples/ # reusable examples
Each subdirectory name matches the corresponding key under components in the OpenAPI document.
In reference/v3.0/tps-afc-api.yaml, reusable payload types live under components/schemas/ as
individual YAML files and are referenced directly by path from root components and from each
other; components/parameters, components/requestBodies, and components/responses are defined
in the root document and point at those files without intermediate components.schemas aliases.
Additional component types may be split into separate files under other components/* keys when reuse across
specs grows.
License
Copyright (c) 2025-2026 Sfey Pay. All rights reserved.
This software is proprietary and forms part of the Sfey Intelligence System (SIS). It is licensed exclusively for internal use by authorized Sfey personnel and systems operating within the AXON (Agentic Execution & Observability Network) infrastructure.
You may not, without prior written authorization from Sfey Pay:
- Copy, reproduce, or distribute this software or any portion of it.
- Modify, adapt, or create derivative works based on this software.
- Reverse-engineer, decompile, or disassemble any part of this software.
- Sublicense, lease, or lend access to any third party.
Unauthorized use, reproduction, or distribution is strictly prohibited and may result in civil liability and criminal penalties under applicable intellectual property law.
For licensing inquiries contact [email protected].